package org.zebra.satoken;

import cn.dev33.satoken.dao.SaTokenDao;
import cn.dev33.satoken.strategy.SaStrategy;
import cn.dev33.satoken.util.SaFoxUtil;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.zebra.cache.util.RedisUtil;

import java.time.Duration;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * 采用 caffeine + redis 多级缓存 优化并发查询效率
 *
 * @author zhanghongbin
 */
public class SaTokenDaoRedisPlus implements SaTokenDao {

    private StringRedisTemplate stringRedisTemplate;
    private RedisTemplate<String, Object> redisTemplate;

    public SaTokenDaoRedisPlus(StringRedisTemplate stringRedisTemplate, RedisTemplate<String, Object> redisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
        this.redisTemplate = redisTemplate;
        SaStrategy.me.createSession = (sessionId) -> new SaSessionForJacksonCustomized(sessionId);
    }

    protected static final Cache<String, Object> CAFFEINE = Caffeine.newBuilder()
            // 设置最后一次写入或访问后经过固定时间过期
            .expireAfterWrite(30, TimeUnit.SECONDS)
            // 初始的缓存空间大小
            .initialCapacity(100)
            // 缓存的最大条数
            .maximumSize(5000)
            .build();

    @Override
    public String get(String key) {
        return (String) CAFFEINE.get(key, k -> RedisUtil.getCacheObject(this.stringRedisTemplate, key));
    }

    /**
     * 写入Value，并设定存活时间 (单位: 秒)
     */
    @Override
    public void set(String key, String value, long timeout) {
        if (timeout == 0 || timeout <= NOT_VALUE_EXPIRE) {
            return;
        }
        // 判断是否为永不过期
        if (timeout == NEVER_EXPIRE) {
            RedisUtil.setCacheObject(this.stringRedisTemplate, key, value);
        } else {
            RedisUtil.setCacheObject(this.stringRedisTemplate, key, value, Duration.ofSeconds(timeout));
        }
        CAFFEINE.put(key, value);
    }

    /**
     * 修修改指定key-value键值对 (过期时间不变)
     */
    @Override
    public void update(String key, String value) {
        long expire = this.getTimeout(key);
        this.set(key, value, expire);
    }

    @Override
    public void delete(String key) {
        CAFFEINE.invalidate(key);
        RedisUtil.delete(this.stringRedisTemplate, key);
    }

    /**
     * 获取Value的剩余存活时间 (单位: 秒)
     */
    @Override
    public long getTimeout(String key) {
        return RedisUtil.getTimeToLive(this.stringRedisTemplate, key);
    }

    /**
     * 修改Value的剩余存活时间 (单位: 秒)
     */
    @Override
    public void updateTimeout(String key, long timeout) {
        CAFFEINE.invalidate(key);
        if (timeout == NEVER_EXPIRE) {
            long expire = this.getTimeout(key);
            if (expire != NEVER_EXPIRE) {
                this.set(key, this.get(key), timeout);
            }
        } else {
            RedisUtil.expire(this.stringRedisTemplate, key, Duration.ofSeconds(timeout));
        }
    }

    @Override
    public Object getObject(String key) {
        return CAFFEINE.get(key, k -> RedisUtil.getCacheObject(this.redisTemplate, key));
    }

    /**
     * 写入Object，并设定存活时间 (单位: 秒)
     */
    @Override
    public void setObject(String key, Object object, long timeout) {
        if (timeout == 0 || timeout <= NOT_VALUE_EXPIRE) {
            return;
        }
        // 判断是否为永不过期
        if (timeout == NEVER_EXPIRE) {
            RedisUtil.setCacheObject(this.redisTemplate, key, object);
        } else {
            RedisUtil.setCacheObject(this.redisTemplate, key, object, Duration.ofSeconds(timeout));
        }
        CAFFEINE.put(key, object);
    }

    @Override
    public void updateObject(String key, Object value) {
        long expire = this.getObjectTimeout(key);
        if (expire != NOT_VALUE_EXPIRE) {
            this.setObject(key, value, expire);
        }
    }

    @Override
    public void deleteObject(String key) {
        CAFFEINE.invalidate(key);
        RedisUtil.delete(this.redisTemplate, key);
    }

    @Override
    public long getObjectTimeout(String key) {
        return RedisUtil.getTimeToLive(this.redisTemplate, key);
    }

    @Override
    public void updateObjectTimeout(String key, long timeout) {
        CAFFEINE.invalidate(key);
        if (timeout == NEVER_EXPIRE) {
            long expire = this.getObjectTimeout(key);
            if (expire != NEVER_EXPIRE) {
                this.setObject(key, this.getObject(key), timeout);
            }
        } else {
            RedisUtil.expire(this.redisTemplate, key, Duration.ofSeconds(timeout));
        }
    }

    @Override
    public List<String> searchData(String prefix, String keyword, int start, int size, boolean sortType) {
        String keyStr = prefix + "*" + keyword + "*";
        return (List<String>) CAFFEINE.get(keyStr, k -> {
            Collection<String> keys = RedisUtil.keys(this.redisTemplate, keyStr);
            List<String> list = new ArrayList<>(keys);
            return SaFoxUtil.searchList(list, start, size, sortType);
        });
    }
}
